package com.tonyodev.fetch2;

import ohos.utils.Pair;
import ohos.utils.zson.ZSONArray;
import ohos.utils.zson.ZSONObject;

import com.tonyodev.fetch2core.Downloader;
import com.tonyodev.fetch2core.Extras;
import com.tonyodev.fetch2core.FetchCoreDefaults;
import com.tonyodev.fetch2core.FetchCoreUtils;
import com.tonyodev.fetch2core.FileResource;
import com.tonyodev.fetch2core.FileServerDownloader;
import com.tonyodev.fetch2core.InterruptMonitor;
import com.tonyodev.fetch2core.MutableExtras;
import com.tonyodev.fetch2core.server.FetchFileResourceTransporter;
import com.tonyodev.fetch2core.server.FileRequest;
import com.tonyodev.fetch2core.server.FileResponse;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import static com.tonyodev.fetch2core.FetchErrorStrings.EMPTY_RESPONSE_BODY;
import static com.tonyodev.fetch2core.FetchErrorStrings.FETCH_FILE_SERVER_INVALID_RESPONSE_TYPE;

/**
 * This downloader is used by Fetch to download files from a Fetch File Server using the
 * Fetch file server url scheme.
 *
 * @since 2021-05-20
 */
public class FetchFileServerDownloader implements FileServerDownloader {
    protected Map<Downloader.Response, FetchFileResourceTransporter> connections = Collections.synchronizedMap(new HashMap<>());
    /**
     * The file downloader type used to download a request.
     * The SEQUENTIAL type downloads byt es in sequence.
     * The PARALLEL type downloads bytes in parallel.
     */
    private Downloader.FileDownloaderType fileDownloaderType = Downloader.FileDownloaderType.SEQUENTIAL;

    /**
     * The timeout value in milliseconds when trying to connect to the server. Default is 20_000 milliseconds.
     */
    private long timeout = 20_000;

    /**
     * create instance
     */
    public FetchFileServerDownloader() {
    }

    /**
     * create instance
     *
     * @param fileDownloaderType fileDownloaderType
     */
    public FetchFileServerDownloader(FileDownloaderType fileDownloaderType) {
        this.fileDownloaderType = fileDownloaderType;
    }

    /**
     * create instance
     *
     * @param timeout timeout
     */
    public FetchFileServerDownloader(long timeout) {
        this.timeout = timeout;
    }

    /**
     * create instance
     *
     * @param fileDownloaderType fileDownloaderType
     * @param timeout timeout
     */
    public FetchFileServerDownloader(FileDownloaderType fileDownloaderType, long timeout) {
        this.fileDownloaderType = fileDownloaderType;
        this.timeout = timeout;
    }

    @Override
    public FileServerDownloader.TransporterRequest onPreClientExecute(FetchFileResourceTransporter client, Downloader.ServerRequest request) {
        Map<String, String> headers = request.getHeaders();
        String tempRange = headers.get("Range");
        tempRange = isEmpty(tempRange) ? "bytes=0-" : tempRange;
        Pair<Long, Long> range = FetchCoreUtils.getRangeForFetchFileServerRequest(tempRange);
        String authorization = headers.get(FileRequest.FIELD_AUTHORIZATION);
        int port = FetchCoreUtils.getFetchFileServerPort(request.getUrl());
        String address = FetchCoreUtils.getFetchFileServerHostAddress(request.getUrl());
        MutableExtras extras = request.getExtras().toMutableExtras();
        Map<String, String> requestHeaders = request.getHeaders();
        for (String header : requestHeaders.keySet()) {
            extras.putString(header, requestHeaders.get(header));
        }
        FileServerDownloader.TransporterRequest transporterRequest = new FileServerDownloader.TransporterRequest();
        transporterRequest.setInetSocketAddress(new InetSocketAddress(address, port));
        FileRequest fileRequest = new FileRequest(
            FileRequest.TYPE_FILE,
            FetchCoreUtils.getFileResourceIdFromUrl(request.getUrl()),
            range.f,
            range.s,
            isEmpty(authorization) ? "" : authorization,
            isEmpty(headers.get(FileRequest.FIELD_CLIENT)) ? UUID.randomUUID().toString() : headers.get(FileRequest.FIELD_CLIENT),
            extras,
            toIntOrNull(headers.get(FileRequest.FIELD_PAGE)),
            toIntOrNull(headers.get(FileRequest.FIELD_SIZE)),
            false);
        transporterRequest.setFileRequest(fileRequest);
        return transporterRequest;
    }

    @Override
    public Downloader.Response execute(ServerRequest request, InterruptMonitor interruptMonitor) throws IOException {
        FetchFileResourceTransporter transporter = new FetchFileResourceTransporter();
        long timeoutStop;
        long timeoutStart = System.nanoTime();
        FileServerDownloader.TransporterRequest transporterRequest = onPreClientExecute(transporter, request);
        transporter.connect(transporterRequest.getInetSocketAddress());
        transporter.sendFileRequest(transporterRequest.getFileRequest());
        while (!interruptMonitor.isInterrupted()) {
            FileResponse serverResponse = transporter.receiveFileResponse();
            if (serverResponse != null) {
                int code = serverResponse.getStatus();
                boolean isSuccessful = serverResponse.getConnection() == FileResponse.OPEN_CONNECTION &&
                    serverResponse.getType() == FileRequest.TYPE_FILE && serverResponse.getStatus() == HttpURLConnection.HTTP_PARTIAL;
                long contentLength = serverResponse.getContentLength();
                InputStream inputStream = transporter.getInputStream();
                String errorResponse;
                if (!isSuccessful) {
                    errorResponse = FetchCoreUtils.copyStreamToString(inputStream, false);
                } else {
                    errorResponse = null;
                }
                Map<String, List<String>> responseHeaders = new LinkedHashMap<>();
                try {
                    ZSONObject json = ZSONObject.stringToZSON(serverResponse.getToJsonString());
                    for (String key : json.keySet()) {
                        System.out.println("JJM FetchFileServerDownloader key =" + json.get(key).toString());
                        responseHeaders.put(key, Collections.singletonList(json.get(key).toString()));
                    }
                } catch (Exception e) {
                    // error
                }
                if (!responseHeaders.containsKey("Content-MD5")) {
                    responseHeaders.put("Content-MD5", Collections.singletonList(serverResponse.getMd5()));
                }
                String hash = getContentHash(responseHeaders);
                List<String> ranges = responseHeaders.get("Accept-Ranges");
                boolean acceptsRanges =
                    code == HttpURLConnection.HTTP_PARTIAL
                        || "bytes".equals(firstOrNull(ranges));
                onServerResponse(request,
                    new Downloader.Response(code,
                        isSuccessful,
                        contentLength,
                        null,
                        request,
                        hash,
                        responseHeaders,
                        acceptsRanges,
                        errorResponse));
                Downloader.Response response = new Downloader.Response(
                    code,
                    isSuccessful,
                    contentLength,
                    inputStream,
                    request,
                    hash,
                    responseHeaders,
                    acceptsRanges,
                    errorResponse);
                connections.put(response, transporter);
                return response;
            }
            timeoutStop = System.nanoTime();
            if (FetchCoreUtils.hasIntervalTimeElapsed(timeoutStart, timeoutStop, timeout)) {
                return null;
            }
        }
        return null;
    }

    @Override
    public void disconnect(Downloader.Response response) {
        if (connections.containsKey(response)) {
            FetchFileResourceTransporter transporter = connections.get(response);
            connections.remove(response);
            if (transporter != null) {
                transporter.close();
            }
        }
    }

    @Override
    public void close() {
        try {
            for (Downloader.Response response : connections.keySet()) {
                FetchFileResourceTransporter transporter = connections.get(response);
                transporter.close();
            }
            connections.clear();
        } catch (Exception e) {
            // error
        }
    }

    @Override
    public Integer getFileSlicingCount(Downloader.ServerRequest request, Long contentLength) {
        return 0;
    }

    @Override
    public Downloader.FileDownloaderType getRequestFileDownloaderType(Downloader.ServerRequest request, Set<Downloader.FileDownloaderType> supportedFileDownloaderTypes) {
        return fileDownloaderType;
    }

    /**
     * verifyContentHash
     *
     * @param request the request information for the download.
     * @param hash Hash returned by the server for the content
     * @return true
     */
    public boolean verifyContentHash(Downloader.ServerRequest request, String hash) {
        if (isEmpty(hash)) {
            return true;
        }
        String fileMd5 = FetchCoreUtils.getFileMd5String(request.getFile());
        if (isEmpty(fileMd5)) {
            return false;
        }
        return fileMd5.contentEquals(hash);
    }

    /**
     * getContentHash
     *
     * @param responseHeaders List of headers from response
     * @return ContentHash
     */
    public String getContentHash(Map<String, List<String>> responseHeaders) {
        List<String> md5s = responseHeaders.get("Content-MD5");
        String md5 = firstOrNull(md5s);
        if (isEmpty(md5)) {
            return "";
        }
        return md5;
    }

    @Override
    public void onServerResponse(Downloader.ServerRequest request, Downloader.Response response) {

    }

    @Override
    public boolean getHeadRequestMethodSupported(Downloader.ServerRequest request) {
        return false;
    }

    @Override
    public long getRequestContentLength(Downloader.ServerRequest request) {
        return FetchCoreUtils.getRequestContentLength(request, this);
    }

    @Override
    public int getRequestBufferSize(ServerRequest request) {
        return FetchCoreDefaults.DEFAULT_BUFFER_SIZE;
    }

    @Override
    public List<FileResource> getFetchFileServerCatalog(Downloader.ServerRequest serverRequest) throws Exception {
        Downloader.Response response = execute(serverRequest, new InterruptMonitor() {
            @Override
            public boolean isInterrupted() {
                return false;
            }
        });
        if (response != null && response.getByteStream() != null) {
            InputStreamReader inputReader = null;
            try {
                Map<String, List<String>> responseHeaders = response.getResponseHeaders();
                List<String> fieldTypes = responseHeaders.get(FileRequest.FIELD_TYPE);
                String fieldType = firstOrNull(fieldTypes);
                int type;
                if (fieldType != null) {
                    type = Integer.parseInt(fieldType);
                } else {
                    type = -1;
                }

                if (type != FileRequest.TYPE_FILE) {
                    disconnect(response);
                    throw new Exception(FETCH_FILE_SERVER_INVALID_RESPONSE_TYPE);
                }
                int bufferSize = 1024;
                char[] buffer = new char[bufferSize];
                StringBuilder stringBuilder = new StringBuilder();
                inputReader = new InputStreamReader(response.getByteStream(), Charset.forName("UTF-8"));
                int read = inputReader.read(buffer, 0, bufferSize);
                while (read != -1) {
                    stringBuilder.append(buffer, 0, read);
                    read = inputReader.read(buffer, 0, bufferSize);
                }
                inputReader.close();
                String data = stringBuilder.toString();
                if (data.length() > 0) {
                    List<FileResource> fileResourceList = parseFileResourceList(data);
                    disconnect(response);
                    return fileResourceList;
                } else {
                    throw new Exception(EMPTY_RESPONSE_BODY);
                }
            } catch (IOException e) {
                disconnect(response);
                throw e;
            } finally {
                if (inputReader != null) {
                    inputReader.close();
                }
            }
        } else {
            throw new Exception(EMPTY_RESPONSE_BODY);
        }
    }

    private List<FileResource> parseFileResourceList(String data) {
        ZSONObject json = ZSONObject.stringToZSON(data);
        ZSONArray catalogArray = ZSONArray.stringToZSONArray(json.getString("catalog"));
        int size = catalogArray.size();
        List<FileResource> fileResourceList = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            FileResource fileResource = new FileResource();
            ZSONObject catalogItem = catalogArray.getZSONObject(i);
            fileResource.setId(catalogItem.getLong("id"));
            fileResource.setName(catalogItem.getString("name"));
            fileResource.setLength(catalogItem.getLong("length"));
            fileResource.setExtras(getExtrasFromCatalogItem(catalogItem));
            fileResource.setMd5(catalogItem.getString("md5"));
            fileResourceList.add(fileResource);
        }
        return fileResourceList;
    }

    private Extras getExtrasFromCatalogItem(ZSONObject catalogItem) {
        Extras extras;
        try {
            Map<String, String> map = new LinkedHashMap<>();
            ZSONObject customJson = ZSONObject.stringToZSON(catalogItem.getString("extras"));
            for (String key : customJson.keySet()) {
                map.put(key, customJson.getString(key));
            }
            extras = new Extras(map);
        } catch (Exception e) {
            extras = Extras.getEmptyExtras();
        }
        return extras;
    }

    /**
     * getRequestSupportedFileDownloaderTypes
     *
     * @param request The request information for the download.
     * @return FileDownloaderTypes
     */
    public Set<Downloader.FileDownloaderType> getRequestSupportedFileDownloaderTypes(Downloader.ServerRequest request) {
        try {
            return FetchCoreUtils.getRequestSupportedFileDownloaderTypes(request, this);
        } catch (Exception e) {
            return Collections.singleton(fileDownloaderType);
        }
    }

    private boolean isEmpty(String text) {
        if (text == null || text.equals("")) {
            return true;
        }
        return false;
    }

    private Integer toIntOrNull(String value) {
        if (value == null || value.length() == 0) {
            return 0;
        }
        return Integer.valueOf(value);
    }

    private <T> T firstOrNull(List<T> list) {
        if (list == null || list.isEmpty()) {
            return null;
        }
        return list.get(0);
    }


}
